#!/usr/bin/env bash
LC_ALL=C
PACKAGE_INSTALLATION_TRIES=0
PACKAGE_INSTALLATION_COUNT=0

readonly VERSION="0.4.6"

# set a github auth token (e.g a PAT ) in DEBGET_TOKEN to get a bigger rate limit
if [ -n "${DEBGET_TOKEN}" ]; then
    export HEADERAUTH="Authorization: token ${DEBGET_TOKEN}"
    export HEADERPARAM="--header"
else
    unset HEADERAUTH
    unset HEADERPARAM
fi

function usage() {
cat <<HELP

Usage

deb-get {update [--repos-only] [--quiet] | upgrade [--dg-only] | show <pkg list> | install <pkg list>
        | reinstall <pkg list> | remove [--remove-repo] <pkg list>
        | purge [--remove-repo] <pkg list>
        | search [--include-unsupported] <regex> | cache | clean
        | list [--include-unsupported] [--raw|--installed|--not-installed]
        | prettylist [<repo>] | csvlist [<repo>] | fix-installed [--old-apps]
        | help | version}

deb-get provides a high-level commandline interface for the package management
system to easily install and update packages published in 3rd party apt
repositories or via direct download.

update
    update is used to resynchronize the package index files from their sources.
    When --repos-only is provided, only initialize and update deb-get's
    external repositories, without updating apt or looking for updates of
    installed packages.
    When --quiet is provided the fetching of deb-get repository updates is done without progress feedback.

upgrade
    upgrade is used to install the newest versions of all packages currently
    installed on the system.
    When --dg-only is provided, only the packages which have been installed by deb-get will be upgraded.

install
    install is followed by one package (or a space-separated list of packages)
    desired for installation or upgrading.

reinstall
    reinstall is followed by one package (or a space-separated list of
    packages) desired for reinstallation.

remove
    remove is identical to install except that packages are removed instead of
    installed. When --remove-repo is provided, also remove the apt repository
    of apt/ppa packages.

purge
    purge is identical to remove except that packages are removed and purged
    (any configuration files are deleted too). When --remove-repo is provided,
    also remove the apt repository of apt/ppa packages.

clean
    clean clears out the local repository (/var/cache/deb-get) of retrieved
    package files.

search
    search for the given regex(7) term(s) from the list of available packages
    supported by deb-get and display matches. When --include-unsupported is
    provided, include packages with unsupported architecture or upstream
    codename and include PPAs for Debian-derived distributions.

show
    show information about the given package (or a space-separated list of
    packages) including their install source and update mechanism.

list
    list the packages available via deb-get. When no option is provided, list
    all supported packages and tell which ones are installed (slower). When
    --include-unsupported is provided, include packages with unsupported
    architecture or upstream codename and include PPAs for Debian-derived
    distributions (faster). When --raw is provided, list all packages and do
    not tell which ones are installed (faster). When --installed is provided,
    only list the packages installed (faster). When --not-installed is provided,
    only list the packages not installed (faster).

prettylist
    markdown formatted list the packages available in repo. repo defaults to
    01-main. If repo is 00-builtin or 01-main the packages from 00-builtin are
    included. Use this to update README.md.

csvlist
    csv formatted list the packages available in repo. repo defaults to
    01-main. If repo is 00-builtin or 01-main the packages from 00-builtin are
    included. Use this with 3rd party wrappers.

cache
    list the contents of the deb-get cache (/var/cache/deb-get).

fix-installed
    fix installed packages whose definitions were changed. When --old-apps is
    provided, transition packages to new format. This command is only intended
    for internal use.

help
    show this help.

version
    show deb-get version.

HELP
}

function package_is_installed() {
    [[ " ${INSTALLED_APPS[*]} " =~ " ${1} " ]]
}

function elevate_privs() {
    if [ "$(id -ru)" -eq 0 ]; then
        # Already in the root context
        ELEVATE=""
    elif command -v doas 1>/dev/null && doas true 2>/dev/null; then
        ELEVATE="doas"
    elif command -v sudo 1>/dev/null && sudo true 2>/dev/null; then
        ELEVATE="sudo"
    else
        fancy_message fatal "$(basename "${0}") requires sudo or doas to elevate permissions, neither were found."
    fi
}

function create_cache_dir() {
    if [ -d /var/cache/get-deb ]; then
        ${ELEVATE} mv /var/cache/get-deb "${CACHE_DIR}"
    fi
    ${ELEVATE} mkdir -p "${CACHE_DIR}" 2>/dev/null
    ${ELEVATE} chmod 755 "${CACHE_DIR}" 2>/dev/null
}

function create_etc_dir() {
    ${ELEVATE} mkdir -p "${ETC_DIR}" 2>/dev/null
    ${ELEVATE} chmod 755 "${ETC_DIR}" 2>/dev/null
}


function unroll_url() {
    # Sourceforge started adding parameters
    local TRIM_URL="$(curl -w "%{url_effective}" -I -L -s -S "${1}" -o /dev/null)"
    printf '%s' "${TRIM_URL/\.deb*/.deb}"
}

function follow_url() {
    local TRIM_URL="$(curl -w "%{url_effective}" -I -L -s -S "${1}" -o /dev/null)"
    printf '%s' "${TRIM_URL}"
}


function get_github_releases() {
    METHOD="github"
    CACHE_FILE="${CACHE_DIR}/${APP}.json_extract"
    # Cache github releases json for 1 hour to try and prevent API rate limits
    #   https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
    #   {"message":"API rate limit exceeded for 62.31.16.154. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)","documentation_url":"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting"}
    #   curl -I https://api.github.com/users/flexiondotorg

    # Do not process github releases while generating a pretty list or upgrading
    if [[ ' install fix-installed ' =~ " ${ACTION} " ]] || { [[ ' update ' =~ " ${ACTION} " ]] && package_is_installed "${APP}"; }; then
        if [ ! -e "${CACHE_FILE}" ] || [ -n "$(find "${CACHE_FILE}" -mmin +"${DEBGET_CACHE_RTN:-60}")" ]; then
            fancy_message info "Updating ${CACHE_FILE}"
            local URL="https://api.github.com/repos/${1}/releases${2:+/$2}"
            wgetcmdarray=(wget  "${HEADERPARAM}" "${HEADERAUTH}" -q --no-use-server-timestamps "${URL}" -O- )
            "${wgetcmdarray[@]}" | ${ELEVATE} tee "${CACHE_FILE}" > /dev/null || ( fancy_message warn "Updating ${CACHE_FILE} failed." )
            if [ -f "${CACHE_FILE}" ] && grep "API rate limit exceeded" "${CACHE_FILE}"; then
                fancy_message warn "Updating ${CACHE_FILE} exceeded GitHub API limits. Deleting it."
                ${ELEVATE} rm "${CACHE_FILE}" 2>/dev/null
            fi
            if [ ! ${APP} == "azuredatastudio" ]; then
                # because MS have to be different
                ${ELEVATE} sed -i '/browser_download/!d;/\.deb/!d' ${CACHE_FILE}
            fi
        fi
    fi
}

function get_gitlab_releases() {
    METHOD="gitlab"
    CACHE_FILE="${CACHE_DIR}/${APP}.json_extract"
    # Cache gitlab releases json for 1 hour to try and prevent API rate limits
    #
    # Do not process gitlab releases while generating a pretty list or upgrading
    # if $1 is org/app or user/repo it must be urlencoded
    # the gitlab api can take a release or use permalink/latest to get the latest release
    # So for gitlab sourced apps that use this release model users can use 99-local to pin a version
    #

    if [[ ' install fix-installed ' =~ " ${ACTION} " ]] || { [[ ' update ' =~ " ${ACTION} " ]] && package_is_installed "${APP}"; }; then
        if [ ! -e "${CACHE_FILE}" ] || [ -n "$(find "${CACHE_FILE}" -mmin +"${DEBGET_CACHE_RTN:-60}")" ]; then
            fancy_message info "Updating ${CACHE_FILE}"
            RELEASE=${2/%latest/permalink/latest}
            if [[ ${1} =~ ^https?://.* ]]; then
                local GITLAB_HOST="$(sed -E 's|(https?://[^/]*).*|\1|' <<< ${1})"
                local GITLAB_PROJECT="$(sed -E 's|https?://[^/]*/||' <<< ${1})"
            else
                local GITLAB_HOST="https://gitlab.com"
                local GITLAB_PROJECT="${1}"
            fi
            if [[ -n "${2}" ]]; then
                local URL="${GITLAB_HOST}/api/v4/projects/${GITLAB_PROJECT//\//%2F}/releases/${RELEASE}/assets/links"
            else
                local URL="${GITLAB_HOST}/api/v4/projects/${GITLAB_PROJECT//\//%2F}/releases"
            fi
            wgetcmdarray=(wget -q --no-use-server-timestamps "${URL}" -O- )
            # Only grab the .deb files URLs, but first add newlines, because the JSON is all on one line.
            # This will make it easier for packages to use sed or grep, instead of needing to use jq.
            "${wgetcmdarray[@]}" | sed -E 's/(\"direct_asset_url\")/\n\1/g; s/(\",)/\1\n/g' | sed -E '/direct_asset_url/!d;/\.deb/!d; s/.*(http.*\.deb).*/\1/' | ${ELEVATE} tee "${CACHE_FILE}" > /dev/null || ( fancy_message warn "Updating ${CACHE_FILE} failed." )
        fi
    fi
}

function get_website() {
    METHOD="website"
    CACHE_FILE="${CACHE_DIR}/${APP}.html"
    if [[ ' install fix-installed ' =~ " ${ACTION} " ]] || { [[ ' update ' =~ " ${ACTION} " ]] && package_is_installed "${APP}"; }; then
        if [ ! -e "${CACHE_FILE}" ] || [ -n "$(find "${CACHE_FILE}" -mmin +"${DEBGET_CACHE_RTN:-60}")" ]; then
            fancy_message info "Updating ${CACHE_FILE}"
            # shellcheck disable=SC2086
            if ! ${ELEVATE} wget ${WGET_VERBOSITY} --no-use-server-timestamps ${WGET_TIMEOUT} "$@" -O "${CACHE_FILE}"; then
                fancy_message warn "Updating ${CACHE_FILE} failed. Deleting it."
                ${ELEVATE} rm -f "${CACHE_FILE}"
            fi
        fi
    fi
}

function fancy_message() {
    if [ -z "${1}" ] || [ -z "${2}" ]; then
      return
    fi

    local RED="\e[31m" GREEN="\e[32m" YELLOW="\e[33m" MAGENTA="\e[35m" RESET="\e[0m"
    MESSAGE_TYPE="${1}"
    MESSAGE="${2}"

    case "${MESSAGE_TYPE}" in
      (info) printf "  [${GREEN}+${RESET}] %s\n" "${MESSAGE}";;
      (progress) printf "  [${GREEN}+${RESET}] %s" "${MESSAGE}";;
      (recommend) printf "  [${MAGENTA}!${RESET}] %s\n" "${MESSAGE}";;
      (warn) printf "  [${YELLOW}*${RESET}] WARNING! %s\n" "${MESSAGE}";;
      (error) printf "  [${RED}!${RESET}] ERROR! %s\n" "${MESSAGE}" >&2;;
      (fatal)
          printf "  [${RED}!${RESET}] ERROR! %s\n" "${MESSAGE}" >&2
          exit 1;;
      (*) printf "  [?] UNKNOWN: %s\n" "${MESSAGE}";;
    esac
}

function download_deb() {
    local URL="${1}"
    local FILE="${2}"

    # shellcheck disable=SC2086
    if ! ${ELEVATE} wget ${WGET_VERBOSITY} --continue ${WGET_TIMEOUT} --show-progress --progress=bar:force:noscroll "${URL}" -O "${CACHE_DIR}/${FILE}"; then
        fancy_message error "Failed to download ${URL}. Deleting ${CACHE_DIR}/${FILE}..."
        ${ELEVATE} rm "${CACHE_DIR}/${FILE}" 2>/dev/null
        return 1
    fi
}

function eula() {
    if [ -n "${EULA}" ] && [ "${DEBIAN_FRONTEND}" != noninteractive ]; then
        echo -e "${EULA}\n\n"
        printf '%s\n' "Do you agree to the ${APP} EULA?"
        select yn in "Yes" "No"; do
            case "$yn" in
                Yes) return 0;;
                No) return 1;;
            esac
        done
    fi
}

function update_apt() {
    ${ELEVATE} apt-get -q -o Dpkg::Progress-Fancy="1" -y update
}

function upgrade_apt() {
    ${ELEVATE} apt-get -q -o Dpkg::Progress-Fancy="1" -y upgrade
}

function upgrade_only_dg() {
    mapfile -t INSTALLED_APT_PPA < <(grep -E "apt\s*$|ppa\s*$" "${ETC_DIR}/installed"); INSTALLED_APT_PPA=(${INSTALLED_APT_PPA[@]%% *})
    printf '%s\0' "${INSTALLED_APT_PPA[@]}" | xargs -0 ${ELEVATE} apt-get -q -o Dpkg::Progress-Fancy="1" -y install --only-upgrade
}

# Update only the added repo (during install action)
function update_only_repo() {
    fancy_message info "Updating: /etc/apt/sources.list.d/${APT_LIST_NAME}.list"
    ${ELEVATE} apt-get update -o Dir::Etc::sourcelist="sources.list.d/${APT_LIST_NAME}.list" \
        -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"
}

function install_apt() {
    ((PACKAGE_INSTALLATION_TRIES++))
    add_apt_repo
    if ! update_only_repo; then
        remove_repo --remove-repo --quiet
        return
    fi

    if ! package_is_installed "${APP}"; then
        if ! eula; then
            remove_repo --remove-repo --quiet
            return
        fi
        if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y install "${APP}"; then
            remove_repo --remove-repo --quiet
            return
        fi
        add_installed
        maint_supported_cache
    else
        if [ "${ACTION}" == "reinstall" ]; then
            if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y --reinstall --allow-downgrades install "${APP}"; then
                return
            fi
        else
            fancy_message info "${APP} is up to date."
        fi
    fi
    ((PACKAGE_INSTALLATION_COUNT++))
}

function install_ppa() {
    ppa_to_apt
    install_apt
}

function install_deb() {
    local URL="${1}"
    local FILE="${FILE:-${URL##*/}}"

    local STATUS=""
    ((PACKAGE_INSTALLATION_TRIES++))

    if ! package_is_installed "${APP}"; then
        if ! eula; then
            return
        fi
        if ! download_deb "${URL}" "${FILE}"; then
            return
        fi
        if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y install "${CACHE_DIR}/${FILE}"; then
            return
        fi
        add_installed
        maint_supported_cache
    else
        if [ "${ACTION}" == "reinstall" ]; then
            if ! download_deb "${URL}" "${FILE}"; then
                return
            fi
            if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y --reinstall --allow-downgrades install "${CACHE_DIR}/${FILE}"; then
                return
            fi
        elif dpkg --compare-versions "${VERSION_PUBLISHED}" gt "${VERSION_INSTALLED}"; then
            if ! download_deb "${URL}" "${FILE}"; then
                return
            fi
            if ! ${ELEVATE} apt-get -q=2 -o Dpkg::Progress-Fancy="1" -y install "${CACHE_DIR}/${FILE}"; then
                return
            fi
        elif [ -z "${FILE}" ]; then
            fancy_message warn "${APP} update check failed, moving on to next package."
        else
            fancy_message info "${FILE} is up to date."
        fi
    fi
    ((PACKAGE_INSTALLATION_COUNT++))
    if [ -f "${CACHE_DIR}/${FILE}" ]; then
        ${ELEVATE} rm "${CACHE_DIR}/${FILE}" 2>/dev/null
    fi
}

function remove_deb() {
    local APP="${1}"
    local REMOVE="${2:-remove}"
    local FILE="${FILE:-${URL##*/}}"
    local STATUS=""

    if package_is_installed "${APP}" || [[ " ${DEPRECATED_INSTALLED[*]} " =~ " ${APP} " ]]; then
        STATUS=$(dpkg-query -Wf '${Status}' "${APP}")
        if [ "${STATUS}" == "deinstall ok config-files" ]; then
            REMOVE="purge"
        fi
        ${ELEVATE} apt-get -q -y --autoremove "${REMOVE}" "${APP}"
        remove_installed "${APP}"
        maint_supported_cache
    else
        fancy_message info "${APP} is not installed."
    fi

    # Remove repos/PPA/key even if the app is not installed.
    case "${METHOD}" in
        direct|github|gitlab|website)
            if [ -f "${CACHE_DIR}/${FILE}" ]; then
                fancy_message info "Removing ${CACHE_DIR}/${FILE}"
                ${ELEVATE} rm "${CACHE_DIR}/${FILE}" 2>/dev/null
            fi
            ;;
        apt|ppa)
            remove_repo "${3}";;
    esac

}

function version_deb() {
    if package_is_installed "${APP}"; then
        dpkg-query -Wf '${Version}' "${APP}" 2> /dev/null
        # else empty output
    fi
}

function info_deb() {
    local INSTALLED="${VERSION_INSTALLED:-No}"
    case "${METHOD}" in
        (direct|github|gitlab|website)
            printf '%s\n' "${PRETTY_NAME}"
            printf '  %s:\t%s\n' Package "${APP}" Repository "${APP_SRC}" Updater deb-get Installed "${INSTALLED}" Published "${VERSION_PUBLISHED}" Architecture "${ARCHS_SUPPORTED}" Download "${URL}" Website "${WEBSITE}" Summary "${SUMMARY}";;
        (apt)
            printf '%s\n' "${PRETTY_NAME}"
            printf '  %s:\t%s\n' Package "${APP}" Repository "${APP_SRC}" Updater apt Installed "${INSTALLED}" Architecture "${ARCHS_SUPPORTED}" Repository "${APT_REPO_URL}" Website "${WEBSITE}" Summary "${SUMMARY}";;
        (ppa)
            printf '%s\n' "${PRETTY_NAME}"
            printf '  %s:\t%s\n' Package "${APP}" Repository "${APP_SRC}" Updater apt Installed "${INSTALLED}" Architecture "${ARCHS_SUPPORTED}" Launchpad "${PPA}" Website "${WEBSITE}" Summary "${SUMMARY}";;
    esac
}

function validate_deb() {
    local FULL_APP="${1}"
    export APP_SRC=${FULL_APP%/*} APP=${FULL_APP##*/}
    export DEFVER=""
    export ASC_KEY_URL="" GPG_KEY_URL="" GPG_KEY_ID=""
    export APT_LIST_NAME="${APP}" APT_REPO_URL="" APT_REPO_OPTIONS="" PPA=""
    export ARCHS_SUPPORTED="amd64" CODENAMES_SUPPORTED=""
    export METHOD="direct"
    export EULA=""
    export VERSION_INSTALLED="" VERSION_PUBLISHED=""
    export PRETTY_NAME="" SUMMARY="" WEBSITE=""
    export URL=""
    export CACHE_FILE="" FILE=""

    # Source the variables
    if [ "${APP_SRC}" == "00-builtin" ]; then
        deb_"${APP}" 2>/dev/null
    else
        . "${ETC_DIR}/${APP_SRC}.d/${APP}" 2>/dev/null
    fi
    if [[ " ${ARCHS_SUPPORTED} " =~ " ${HOST_ARCH} " ]] && { [ -z "${CODENAMES_SUPPORTED}" ] || [[ " ${CODENAMES_SUPPORTED} " =~ " ${UPSTREAM_CODENAME} " ]]; } && { [ "${METHOD}" != ppa ] || [ "${UPSTREAM_ID}" == ubuntu ]; }; then

        if [ -z "${DEFVER}" ] || [ -z "${PRETTY_NAME}" ] || [ -z "${SUMMARY}" ] || [ -z "${WEBSITE}" ]; then
            fancy_message error "Missing required information of package ${APP}:"
            printf >&2 '%s\n' "DEFVER=${DEFVER}" "PRETTY_NAME=${PRETTY_NAME}" "SUMMARY=${SUMMARY}" "WEBSITE=${WEBSITE}"
            return 1
        fi
        VERSION_INSTALLED=$(version_deb)
        if [ -n "${APT_REPO_URL}" ]; then
            METHOD="apt"
            if [ "${ACTION}" != "prettylist" ]; then
                if [ -z "${ASC_KEY_URL}" ] && [ -z "${GPG_KEY_URL}" ] && [ -z "${GPG_KEY_ID}" ]; then
                    fancy_message error "Missing required information of apt package ${APP}:"
                    printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}"
                    return 1
                fi
                if [ -n "${ASC_KEY_URL}" ] && [ -n "${GPG_KEY_URL}" ]; then
                    fancy_message error "Conflicting repository key types for apt package ${APP}:"
                    printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}"
                    return 1
                fi
                if [ -n "${GPG_KEY_URL}" ] && [ -n "${GPG_KEY_ID}" ]; then
                    fancy_message error "Conflicting repository key types for apt package ${APP}:"
                    printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}"
                    return 1
                fi
                if [ -n "${ASC_KEY_URL}" ] && [ -n "${GPG_KEY_ID}" ]; then
                    fancy_message error "Conflicting repository key types for apt package ${APP}:"
                    printf >&2 '%s\n' "ASC_KEY_URL=${ASC_KEY_URL}" "GPG_KEY_URL=${GPG_KEY_URL}" "GPG_KEY_ID=${GPG_KEY_ID}"
                    return 1
                fi
            fi
        elif [ -n "${PPA}" ]; then
            METHOD="ppa"
        else
            # If the method is github and the cache file is empty, ignore the package
            # The GitHub API is rate limit has likely been reached
            if [ "${METHOD}" == github ] && [ ! -s "${CACHE_FILE}" ]; then
                if ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]]; then
                    fancy_message warn "Cached file ${CACHE_FILE} is empty or missing."
                    ${ELEVATE} rm "${CACHE_FILE}" 2>/dev/null
                fi
            fi

            if { { [[ ' github website ' =~ " ${METHOD} " ]] && [ -s "${CACHE_FILE}" ]; } || [ "${METHOD}" == direct ]; } &&
            ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]] &&
            { [ -z "${URL}" ] || [ -z "${VERSION_PUBLISHED}" ]; }; then
                fancy_message error "Missing required information of ${METHOD} package ${APP}:"
                printf >&2 '%s\n' "URL=${URL}" "VERSION_PUBLISHED=${VERSION_PUBLISHED}"
                return 1
            fi
        fi
    elif [ -n "${PPA}" ]; then
        METHOD="ppa"
    else
        # If the method is github and the cache file is empty, ignore the package
        # The GitHub API is rate limit has likely been reached
        if [ "${METHOD}" == github ] && [ ! -s "${CACHE_FILE}" ]; then
            if ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]]; then
                fancy_message warn "Cached file ${CACHE_FILE} is empty or missing."
                ${ELEVATE} rm "${CACHE_FILE}" 2>/dev/null
            fi
        fi

        if { { [[ ' github website gitlab ' =~ " ${METHOD} " ]] && [ -s "${CACHE_FILE}" ]; } || [ "${METHOD}" == direct ]; } &&
           ! [[ ' prettylist remove purge ' =~ " ${ACTION} " ]] &&
           { [ -z "${URL}" ] || [ -z "${VERSION_PUBLISHED}" ]; } &&
           { [ -z "${ARCHS_SUPPORTED}" ] || [[ " ${ARCHS_SUPPORTED} " =~ " ${HOST_ARCH} " ]]; } &&
           { [ -z "${CODENAMES_SUPPORTED}" ] || [[ " ${CODENAMES_SUPPORTED} " =~ " ${UPSTREAM_CODENAME} " ]]; }; then
            fancy_message error "Missing required information of ${METHOD} package ${APP}:"
            printf >&2 '%s\n' "URL=${URL}" "VERSION_PUBLISHED=${VERSION_PUBLISHED}"
            return 1
        fi
    fi
    return 0
}

function list_debs() {
    if [ "${1}" == --include-unsupported ]; then
        case "${2}" in
        (--raw)
            printf '%s\n' "${APPS[@]##*/}"
            ;;
        (--installed)
            printf '%s\n' "${INSTALLED_APPS[@]}"
            ;;
        (--not-installed)
            printf '%s\n' "${APPS[@]##*/}" | comm -23  - <(printf '%s\n' "${INSTALLED_APPS[@]}")
            ;;
        (*)
            local PAD='                              '
            for FULL_APP in "${APPS[@]}"; do
                local APP=${FULL_APP##*/}
                if package_is_installed "${APP}"; then
                    printf '%s %s [ installed ]\n' "${APP}" "${PAD:${#APP}}"
                else
                    printf '%s\n' "${APP}"
                fi
            done
        esac
    else
        if [ -f "${CACHE_DIR}"/supported.list ] ; then
            case "${2}" in
            (--raw)
                list_debs --include-unsupported --raw | comm --nocheck-order -12 ${CACHE_DIR}/supported_apps.list -
                ;;
            (--installed)
                # these don't have the [installed] tag so need a similar file to join
                list_debs --include-unsupported --installed | comm  --nocheck-order -12 ${CACHE_DIR}/supported_apps.list -
                ;;
            (--not-installed)
                list_debs --include-unsupported --not-installed | comm --nocheck-order -12 ${CACHE_DIR}/supported_apps.list -
                ;;
            (--only-unsupported)
                list_debs --include-unsupported --raw | comm --nocheck-order -13 ${CACHE_DIR}/supported_apps.list -
                ;;
            (*)
                # this has [ installed ] tags
                list_debs --include-unsupported  | comm --nocheck-order -12 ${CACHE_DIR}/supported.list -
                ;;
            esac
        else
            #elevate_privs
            # because we need to update the cache files this one slow time
            for FULL_APP in "${APPS[@]}"; do
                if validate_deb "${FULL_APP}"; then
                    if [[ " ${ARCHS_SUPPORTED} " =~ " ${HOST_ARCH} " ]] && { [ -z "${CODENAMES_SUPPORTED}" ] || [[ " ${CODENAMES_SUPPORTED} " =~ " ${UPSTREAM_CODENAME} " ]]; } && { [ "${METHOD}" != ppa ] || [ "${UPSTREAM_ID}" == ubuntu ]; }; then
                        case "${2}" in
                        (--raw)
                            printf '%s\n' "${APP}"
                            ;;
                        (--installed)
                            if package_is_installed "${APP}"; then
                                printf '%s\n' "${APP}"
                            fi
                            ;;
                        (--not-installed)
                            if ! package_is_installed "${APP}"; then
                                printf '%s\n' "${APP}"
                            fi
                            ;;
                        (*)
                            if package_is_installed "${APP}"; then
                                local PAD='                              '
                                printf "%s %s [ installed ]\n" "${APP}" "${PAD:${#APP}}"
                            else
                                printf '%s\n' "${APP}"
                            fi
                            ;;
                        esac
                    fi
                fi

            done
        fi
    fi
}

function prettylist_debs() {
    local REPO="${1:-01-main}"
    local ICON=""
cat <<"EOMSG"
| Source   | Package Name   | Description   |
| :------: | :------------- | :------------ |
EOMSG
    for FULL_APP in "${APPS[@]}"; do
        validate_deb "${FULL_APP}"
        if [ "${APP_SRC}" == "${REPO}" ] || { [ "${REPO}" == "01-main" ] && [ "${APP_SRC}" == "00-builtin" ]; }; then
            case "${METHOD}" in
                apt)    ICON="debian.png";;
                github) ICON="github.png";;
                gitlab) ICON="gitlab.png";;
                ppa)    ICON="launchpad.png";;
                *)      ICON="direct.png";;
            esac
            echo "| [<img src=\"../.github/${ICON}\" align=\"top\" width=\"20\" />](${WEBSITE}) | "'`'"${APP}"'`'" | <i>${SUMMARY}</i> |"
        fi
    done
}

function csvlist_debs() {
    local REPO="${1:-01-main}"
    for FULL_APP in "${APPS[@]}"; do
        if validate_deb "${FULL_APP}"; then
            if [ "${APP_SRC}" == "${REPO}" ] || { [ "${REPO}" == "01-main" ] && [ "${APP_SRC}" == "00-builtin" ]; }; then
                echo "\"${APP}\",\"${PRETTY_NAME}\",\"${VERSION_INSTALLED}\",\"${ARCHS_SUPPORTED}\",\"${METHOD}\",\"${SUMMARY}\""
            fi
        fi
    done
}

function update_debs() {
    local STATUS=""
    update_apt
    for APP in "${INSTALLED_APPS[@]}"; do
        validate_deb "${APPNAME2FULL[$APP]}"
        if [ "${METHOD}" == "direct" ] || [ "${METHOD}" == "github" ] || [ "${METHOD}" == "gitlab" ] || [ "${METHOD}" == "website" ]; then
            STATUS=$(dpkg-query -Wf '${Status}' "${APP}")
            if [ "${STATUS}" == "install ok installed" ] && dpkg --compare-versions "${VERSION_PUBLISHED}" gt "${VERSION_INSTALLED}"; then
                fancy_message info "${APP} (${VERSION_INSTALLED}) has an update pending. ${VERSION_PUBLISHED} is available."
            fi
        fi
    done
}

function upgrade_debs() {
    local STATUS=""
    if [[ " $* " != *' --dg-only '* ]] ; then
        upgrade_apt
    else
        upgrade_only_dg
    fi
    for APP in "${INSTALLED_APPS[@]}"; do
        validate_deb "${APPNAME2FULL[$APP]}"
        if [ "${METHOD}" == "direct" ] || [ "${METHOD}" == "github" ]|| [ "${METHOD}" == "gitlab" ] || [ "${METHOD}" == "website" ]; then
            STATUS=$(dpkg-query -Wf '${Status}' "${APP}")
            if [ "${STATUS}" == "install ok installed" ]; then
                install_deb "${URL}"
            fi
        fi
    done
}

function init_repos() {
    if [ ! -e "${ETC_DIR}/01-main.repo" ]; then
        ${ELEVATE} tee "${ETC_DIR}/01-main.repo" <<<"${MAIN_REPO_URL}" >/dev/null
    fi

    for REPO in $(find "${ETC_DIR}" -maxdepth 1 -name "*.repo" ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf "%f\n" | sed "s/.repo$//"); do
        if [ ! -e "${ETC_DIR}/${REPO}.d" ]; then
            ${ELEVATE} mkdir "${ETC_DIR}/${REPO}.d" 2>/dev/null
            ${ELEVATE} chmod 755 "${ETC_DIR}/${REPO}.d" 2>/dev/null
        fi
    done
}

function refresh_supported_cache_lists() {
    # WARN: this function must run in a subshell
    local lockfile=${CACHE_DIR}/updating_supported.lock
    if [ -f "$lockfile" ]; then
        pgrep -f "$DEBGET_BIN update"
        if [ $? ]; then
            # pgrep returned 1 (command not found), delete lockfile and continue
            fancy_message warn "Lock file found, but job is not running. Deleting $lockfile, cache update continues."
            ${ELEVATE} rm $lockfile
        else
            # pgrep returned 0 (command found), do nothing and return
            fancy_message warn "Cannot update cache of supported apps: $lockfile found (job still running?)"
            return 0
        fi
    fi

    trap "trap - EXIT; ${ELEVATE} rm -f \"$lockfile\"" EXIT
    ${ELEVATE} touch "$lockfile"
    ${ELEVATE} rm -f "${CACHE_DIR}/supported.list" "${CACHE_DIR}/supported_apps.list"
    fancy_message info "Updating cache of supported apps in the background"
    list_debs | grep -v -e '^[[:space:]][[:space:]]*\[' | ${ELEVATE} tee "${CACHE_DIR}/supported.list.tmp" >/dev/null
    ${ELEVATE} mv "${CACHE_DIR}/supported.list.tmp" "${CACHE_DIR}/supported.list"
    # # belt and braces no longer needed
    #${ELEVATE} sed -i '/[+]/d' ${CACHE_DIR}/supported.list.tmp
    cut -d" " -f 1 "${CACHE_DIR}/supported.list" | sort -u | ${ELEVATE} tee "${CACHE_DIR}/supported_apps.list" >/dev/null
}

function update_repos() {
    local REPO_URL=""
    # preserve current behaviour for now but allow modification via env
    local CURL_OPTS="--disable --progress-bar"
    local WGET_VERBOSITY="--quiet"

    if [[ " $* " == *' --quiet'* ]]  ; then
         CURL_OPTS="--disable --show-error --silent"
    fi

    for REPO in $(find "${ETC_DIR}" -maxdepth 1 -name "*.repo" ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf "%f\n" | sed "s/.repo$//"); do
        # export REPO ETC_DIR ELEVATE  # no longer needed, `| bash -` replaced with `eval`
        fancy_message info "Updating ${ETC_DIR}/${REPO}"
        REPO_URL="$(head -n 1 "${ETC_DIR}/${REPO}.repo")"
        # shellcheck disable=SC2086
        ${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} "${REPO_URL}/manifest" -O "${ETC_DIR}/${REPO}.repo.tmp" && ${ELEVATE} mv "${ETC_DIR}/${REPO}.repo.tmp" "${ETC_DIR}/${REPO}.repo"

        # ${ELEVATE} rm "${ETC_DIR}/${REPO}.d/* # we currently leave old litter : either <- this or maybe rm older ones
        # although so long as manifest is good we are OK
        # Faster by some margin if we are hitting github
        # Otherwise revert to old-style for a bespoke hosted repo

        pushd "${ETC_DIR}/${REPO}.d" >/dev/null
        (eval "$(
        awk -F/ '/github/  {print "# fetching github repo";
                            print "GITREPO="$4"/"$5;\
                            print "BRANCH="$6;\
                            print "curl ${CURL_OPTS} -L https://api.github.com/repos/${GITREPO}/tarball/${BRANCH} | ${ELEVATE} tar zx --wildcards \"*/${REPO}*/packages/*\"   --strip-components=3"}
                ! /github/ {print "# fetching non-github repo";
                            print "tail -n +2 \"${ETC_DIR}/${REPO}.repo\" | sed \"s/^#//\" | ${ELEVATE} sort -u -o \"${ETC_DIR}/${REPO}.repo.tmp\"";\
                            print "${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} -N -B \"${REPO_URL}/packages/\" -i \"${ETC_DIR}/${REPO}.repo.tmp\" -P \"${ETC_DIR}/${REPO}.d\"";
                            print "${ELEVATE} rm \"${ETC_DIR}/${REPO}.repo.tmp\""
                } '\
                <<<"${REPO_URL}"
        )")

        popd >/dev/null
    done
    refresh_supported_cache_lists &
}

function list_repo_apps() {
    if [ -d "${ETC_DIR}" ]; then
        find "${ETC_DIR}" -maxdepth 1 -name '*.repo' ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf '%f\n' |
        sort -r |
        while IFS= read -r REPO; do
            # WARN: repos can't contain '/' or '\', which the rest of the code assumes anyway
            sed -n -e "1d; /^#/d; s/^/${REPO%.repo}\//p" <"${ETC_DIR}/${REPO}" | sort -u
        done
    fi
}

function populate_deprecated_apps() {
    declare -ga DEPRECATED_APPS=() DEPRECATED_INSTALLED=()
    if [ -d "${ETC_DIR}" ]; then
        mapfile -t DEPRECATED_APPS < <(
        find "${ETC_DIR}" -maxdepth 1 -name "*.repo" ! -name 00-builtin.repo ! -name 99-local.repo -type f -printf "%f\n" |
        sort -r |
        while IFS= read -r REPO; do
            # sort -t ... -u may fail
            sed -n -e "1d; s/^#/${REPO%.repo}\//p" <"${ETC_DIR}/${REPO}" | sort -t / -k 2 | uniq
        done
        )
        if [ "${#DEPRECATED_APPS[@]}" -gt 0 ]; then
            mapfile -t DEPRECATED_INSTALLED < <(dpkg-query 2>/dev/null -Wf '${db:Status-abbrev}${Package}\n' "${DEPRECATED_APPS[@]##*/}" | sed -n -e 's/^ii //p')
        fi
    fi
    readonly DEPRECATED_INSTALLED
}

function list_local_apps() {
    if [ -d "${ETC_DIR}/99-local.d" ]; then
        find "${ETC_DIR}/99-local.d" -maxdepth 1 -type f -printf '99-local/%f\n'
    fi
}

function print_etc_overrides() {
    if [ "${#LOCAL_APPS[@]}" -gt 0 ] || [ "${#APP_CONFLICTS[@]}" -gt 0 ]; then
        local DEB_GET_SCRIPT_FILE="${0}"
        local NUM_OLDER_CONFLICTS=0
        for APP in "${APP_CONFLICTS[@]}"; do
            local FULL_APP=${APPNAME2FULL[$APP]}
            fancy_message warn "Conflict detected, duplicate declaration of package ${APP}, using declaration from ${FULL_APP%/*}"

            if [[ " ${LOCAL_APPS[*]} " =~ " ${FULL_APP} " ]] && [ "${DEB_GET_SCRIPT_FILE}" -nt "${ETC_DIR}/99-local.d/${APP}" ]; then
                ((NUM_OLDER_CONFLICTS++))
            fi
        done

        if [ "${NUM_OLDER_CONFLICTS}" -gt 0 ]; then
            fancy_message recommend "Duplicate entr(ies) already merged upstream (if no longer needed), must be manually removed from your ${ETC_DIR}/99-local.d folder."
        fi

        for FULL_APP in "${LOCAL_APPS[@]}"; do
            fancy_message info "Including local package ${FULL_APP##*/}"
        done

        if [ "${#LOCAL_APPS[@]}" -gt 0 ]; then
            fancy_message recommend "Please consider contributing back new entries, an issue (or raise a PR) directly at https://github.com/wimpysworld/deb-get/pulls"
        fi
    fi
}

function print_deprecated() {
    for APP in "${DEPRECATED_INSTALLED[@]}"; do
        fancy_message warn "Deprecated package ${APP} detected. It will no longer receive updates, and keeping it installed is considered unsafe."
        fancy_message recommend "Please remove it with: deb-get purge ${APP}"
    done
}

function fix_old_apps() {
    local OLD_METHOD=""
    local OLD_APT_LIST_NAME=""
    local OLD_PPA=""
    case "${APP}" in
        1password)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="1password"
        ;;
        anydesk)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="anydesk-stable"
        ;;
        appimagelauncher)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:appimagelauncher-team/stable"
        ;;
        atom)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="atom"
        ;;
        audio-recorder)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:audio-recorder/ppa"
        ;;
        azure-cli)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="azure-cli"
        ;;
        blanket)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:apandada1/blanket"
        ;;
        brave-browser)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="brave-browser-release"
        ;;
        cawbird)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="home:IBBoard:cawbird"
        ;;
        chronograf)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="influxdata"
        ;;
        code)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="vscode"
        ;;
        copyq)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:hluk/copyq"
        ;;
        cryptomator)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:sebastian-stenzel/cryptomator"
        ;;
        docker-ce)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="docker"
        ;;
        enpass)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="enpass"
        ;;
        firefox-esr)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:mozillateam/ppa"
        ;;
        foliate)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:apandada1/foliate"
        ;;
        fsearch)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:christian-boxdoerfer/fsearch-stable"
        ;;
        google-chrome-stable)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="google-chrome"
        ;;
        google-earth-pro-stable)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="google-earth-pro"
        ;;
        gpu-viewer)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:arunsivaraman/gpuviewer"
        ;;
        influxdb)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="influxdata"
        ;;
        influxdb2)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="influxdata"
        ;;
        influxdb2-cli)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="influxdata"
        ;;
        insync)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="insync"
        ;;
        jellyfin)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="jellyfin"
        ;;
        kapacitor)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="influxdata"
        ;;
        kdiskmark)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:jonmagon/kdiskmark"
        ;;
        keepassxc)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:phoerious/keepassxc"
        ;;
        keybase)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="keybase"
        ;;
        kopia-ui)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="kopia"
        ;;
        lutris)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:lutris-team/lutris"
        ;;
        microsoft-edge-stable)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="microsoft-edge"
        ;;
        neo4j)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="neo4j"
        ;;
        nextcloud-desktop)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:nextcloud-devs/client"
        ;;
        nomad)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="nomad"
        ;;
        obs-studio)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:flexiondotorg/obs-fully-loaded"
        ;;
        openrazer-meta)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:openrazer/stable"
        ;;
        opera-stable)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="opera-stable"
        ;;
        papirus-icon-theme)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:papirus/papirus"
        ;;
        plexmediaserver)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="plexmediaserver"
        ;;
        polychromatic)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:polychromatic/stable"
        ;;
        protonvpn)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="protonvpn-stable"
        ;;
        qownnotes)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:pbek/qownnotes"
        ;;
        quickemu)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:flexiondotorg/quickemu"
        ;;
        quickgui)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:yannick-mauray/quickgui"
        ;;
        resilio-sync)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="resilio-sync"
        ;;
        retroarch)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:libretro/stable"
        ;;
        signal-desktop)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="signal-xenial.list"
        ;;
        skypeforlinux)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="skype-stable"
        ;;
        slack-desktop)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="slack"
        ;;
        softmaker-office-2021)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="softmaker"
        ;;
        strawberry)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:jonaski/strawberry"
        ;;
        sublime-merge)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="sublime-text"
        ;;
        sublime-text)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="sublime-text"
        ;;
        syncthing)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="syncthing"
        ;;
        teams)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="teams"
        ;;
        telegraf)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="influxdata"
        ;;
        terraform)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="terraform"
        ;;
        texworks)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:texworks/stable"
        ;;
        typora)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="typora"
        ;;
        ubuntu-make)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:lyzardking/ubuntu-make"
        ;;
        ulauncher)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:agornostal/ulauncher"
        ;;
        virtualbox-6.1)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="virtualbox-6.1"
        ;;
        vivaldi-stable)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="vivaldi"
        ;;
        wavebox)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="wavebox-stable"
        ;;
        weechat)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="weechat"
        ;;
        wire-desktop)
            OLD_METHOD="apt"
            OLD_APT_LIST_NAME="wire-desktop"
        ;;
        xemu)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:mborgerson/xemu"
        ;;
        yq)
            OLD_METHOD="ppa"
            OLD_PPA="ppa:rmescandon/yq"
        ;;
    esac
    if [ -n "${OLD_METHOD}" ]; then
        if [ "${OLD_METHOD}" = apt ]; then
            remove_old_apt_repo "${OLD_APT_LIST_NAME}"
        else # ppa
            remove_old_ppa_repo "${OLD_PPA}"
        fi
    fi
    if [ "${METHOD}" = apt ]; then
        add_apt_repo
    elif [ "${METHOD}" = ppa ]; then
        ppa_to_apt
        add_apt_repo
    fi
    add_installed

    if [ "${DEFVER}" != 1 ]; then
        fancy_message warn "${APP} must be manually reinstalled with \"deb-get reinstall ${APP}\", otherwise it will not be updated properly"
    fi
}

function fix_installed() {
    local line="$(grep -m 1 "^${APP} " "${ETC_DIR}/installed")"
    local OLD_DEFVER="$(cut -d " " -f 2 <<<"$line")"
    local OLD_METHOD="$(cut -d " " -f 3 <<<"$line")"
    if [ "${DEFVER}" != "${OLD_DEFVER}" ]; then
        remove_installed "${APP}"
        if [[ " apt ppa " =~ " ${OLD_METHOD} " ]]; then
            remove_repo --remove-repo
        fi
        if [ "${METHOD}" = apt ]; then
            add_apt_repo
        elif [ "${METHOD}" = ppa ]; then
            ppa_to_apt
            add_apt_repo
        fi
        add_installed
        fancy_message warn "${APP} must be manually reinstalled with \"deb-get reinstall ${APP}\", otherwise it will not be updated properly"
    fi
}

function remove_old_apt_repo() {
    fancy_message info "Removing /etc/apt/trusted.gpg.d/${1}.asc"
    ${ELEVATE} rm -f "/etc/apt/trusted.gpg.d/${1}.asc"
    fancy_message info "Removing /etc/apt/sources.list.d/${1}.list"
    ${ELEVATE} rm -f "/etc/apt/sources.list.d/${1}.list"
}

function remove_old_ppa_repo() {
    local -r PPA_ADDRESS=${1#*:}
    local -r PPA_PERSON=${PPA_ADDRESS%%/*}
    local -r PPA_ARCHIVE=${PPA_ADDRESS#*/}
    local -r APT_LIST_NAME="${PPA_PERSON}-ubuntu-${PPA_ARCHIVE}"
    fancy_message info "Removing /etc/apt/trusted.gpg.d/${APT_LIST_NAME}.gpg"
    ${ELEVATE} rm -f "/etc/apt/trusted.gpg.d/${APT_LIST_NAME}.gpg"
    ${ELEVATE} rm -f "/etc/apt/trusted.gpg.d/${APT_LIST_NAME}.gpg~"
    fancy_message info "Removing /etc/apt/sources.list.d/${APT_LIST_NAME}-${UPSTREAM_CODENAME}.list"
    ${ELEVATE} rm -f "/etc/apt/sources.list.d/${APT_LIST_NAME}-${UPSTREAM_CODENAME}.list"
}

function remove_repo() {
    if [[ -n "${PPA}" ]]; then
        local -r PPA_ADDRESS=${PPA#*:}
        local -r PPA_PERSON=${PPA_ADDRESS%%/*}
        local -r PPA_ARCHIVE=${PPA_ADDRESS#*/}
        APT_LIST_NAME="${PPA_PERSON}-ubuntu-${PPA_ARCHIVE}-${UPSTREAM_CODENAME}"
    fi
    local count=""
    if [ -e "${ETC_DIR}/aptrepos" ]; then
        count="$(grep -m 1 "^${APT_LIST_NAME} " "${ETC_DIR}/aptrepos" | cut -d " " -f 2)"
    fi
    if [ -z "${count}" ]; then
        count=0
    fi
    if [ "${count}" -gt 0 ]; then
        ((count--))
        ${ELEVATE} sed -i -E "/^${APT_LIST_NAME} [0-9]+/d" "${ETC_DIR}/aptrepos"
        ${ELEVATE} tee -a "${ETC_DIR}/aptrepos" <<<"${APT_LIST_NAME} ${count}" >/dev/null
    fi
    if [ "${1}" == --remove-repo ]; then
        if [ "${count}" -eq 0 ]; then
            if [ "${2}" != --quiet ]; then
                fancy_message info "Removing /usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg"
            fi
            ${ELEVATE} rm -f "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg"
            if [ "${2}" != --quiet ]; then
                fancy_message info "Removing /etc/apt/sources.list.d/${APT_LIST_NAME}.list"
            fi
            ${ELEVATE} rm -f "/etc/apt/sources.list.d/${APT_LIST_NAME}.list"
            if [ -e "${ETC_DIR}/aptrepos" ]; then
                ${ELEVATE} sed -i -E "/^${APT_LIST_NAME} [0-9]+/d" "${ETC_DIR}/aptrepos"
            fi
        elif [ "${2}" != --quiet ]; then
            fancy_message warn "/etc/apt/sources.list.d/${APT_LIST_NAME}.list was not removed because other packages depend on it."
        fi
    fi
}

function add_apt_repo() {
    if [[ "${ACTION}" != "reinstall" ]]; then
        local count=""
        if [ -e "${ETC_DIR}/aptrepos" ]; then
            count="$(grep -m 1 "^${APT_LIST_NAME} " "${ETC_DIR}/aptrepos" | cut -d " " -f 2)"
        fi
        if [ -z "${count}" ]; then
            count=0
        fi
        if [ "${count}" -eq 0 ] && [ -e "/etc/apt/sources.list.d/${APT_LIST_NAME}.list" ]; then
            ((count++))
        fi
        ((count++))
        if [ -e "${ETC_DIR}/aptrepos" ]; then
            ${ELEVATE} sed -i -E "/^${APT_LIST_NAME} [0-9]+/d" "${ETC_DIR}/aptrepos"
        fi
        ${ELEVATE} tee -a "${ETC_DIR}/aptrepos" <<<"${APT_LIST_NAME} ${count}" >/dev/null
    fi
    if [ ! -d /usr/share/keyrings ]; then
        ${ELEVATE} mkdir -p /usr/share/keyrings 2>/dev/null
    fi
    if [ ! -e "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" ]; then
        if [ -n "${ASC_KEY_URL}" ]; then
            # shellcheck disable=SC2086
            ${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} "${ASC_KEY_URL}" -O "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring"
            ${ELEVATE} gpg --yes --dearmor "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring"
            ${ELEVATE} rm "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring"
        elif [ -n "${GPG_KEY_ID}" ]; then
            #Fetching from the keyserver will fail if root doesn't already have a .gnupg directory.
            #This will create it if it doesn't already exist.
            if ${ELEVATE} printenv GNUPGHOME > /dev/null && ${ELEVATE} [ ! -e "${GNUPGHOME}" ] || ${ELEVATE} [ ! -e "$(${ELEVATE} printenv HOME)/.gnupg" ] ; then
               ${ELEVATE} gpg --list-keys
            fi
            ${ELEVATE} gpg --no-default-keyring --keyring /usr/share/keyrings/"${APT_LIST_NAME}"-archive-keyring-temp.gpg --keyserver keyserver.ubuntu.com --recv "${GPG_KEY_ID}"
            ${ELEVATE} gpg --no-default-keyring --keyring /usr/share/keyrings/"${APT_LIST_NAME}"-archive-keyring-temp.gpg --yes --output "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg" --export "${GPG_KEY_ID}"
            ${ELEVATE} rm /usr/share/keyrings/"${APT_LIST_NAME}"-archive-keyring-temp.gpg
        else #GPG_KEY_URL
            # shellcheck disable=SC2086
            ${ELEVATE} wget ${WGET_VERBOSITY} ${WGET_TIMEOUT} "${GPG_KEY_URL}" -O "/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg"
        fi
    fi

    local APT_LIST_LINE="deb [signed-by=/usr/share/keyrings/${APT_LIST_NAME}-archive-keyring.gpg"

    if [ -n "${APT_REPO_OPTIONS}" ]; then
        APT_LIST_LINE="${APT_LIST_LINE} ${APT_REPO_OPTIONS}"
    fi

    APT_LIST_LINE="${APT_LIST_LINE}] ${APT_REPO_URL}"
    ${ELEVATE} tee "/etc/apt/sources.list.d/${APT_LIST_NAME}.list" <<<"${APT_LIST_LINE}" >/dev/null
}

function ppa_to_apt() {
    local -r PPA_ADDRESS=${PPA#*:}
    local -r PPA_PERSON=${PPA_ADDRESS%%/*}
    local -r PPA_ARCHIVE=${PPA_ADDRESS#*/}
    export APT_REPO_URL="https://ppa.launchpadcontent.net/${PPA_ADDRESS}/ubuntu/ ${UPSTREAM_CODENAME} main"
    export ASC_KEY_URL="https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(curl -s "https://api.launchpad.net/devel/~${PPA_PERSON}/+archive/ubuntu/${PPA_ARCHIVE}" | grep -o -E "\"signing_key_fingerprint\": \"[0-9A-F]+\"" | cut -d \" -f 4)"
    export APT_LIST_NAME="${PPA_PERSON}-ubuntu-${PPA_ARCHIVE}-${UPSTREAM_CODENAME}"
}

function maint_supported_cache() {
    # called by install and re-install when we've installed
    # so we should be supported

    if [ -f "${CACHE_DIR}/supported.list" ]; then
        case "${ACTION}" in
            remove|purge)
                ${ELEVATE} sed -i "/^${APP} /d" "${CACHE_DIR}/supported.list"
                cat "${CACHE_DIR}/supported.list" - <<<"${APP}" | ${ELEVATE} sort -t " " -k 1 -u -o "${CACHE_DIR}/supported.list"
                ;;
            reinstall|install)
                local PAD='                              '
                local cache_line=$(printf "%s %s [ installed ]\n" "${APP}" "${PAD:${#APP}}")
                # # First remove the bare entry
                ${ELEVATE} sed -i -e '/^${APP}$/d' "${CACHE_DIR}/supported.list"
                # Replace it with a flagged one
                cat "${CACHE_DIR}/supported.list" - <<<"${cache_line}" | ${ELEVATE} sort -t " " -k 1 -u -o "${CACHE_DIR}/supported.list"
                # should be there but safest to be sure
                grep -q -w "${APP}$" "${CACHE_DIR}"/supported_apps.list || \
                cat ${CACHE_DIR}/supported_apps.list - <<<"${APP}" | ${ELEVATE} sort -t " " -k 1 -u -o ${CACHE_DIR}/supported_apps.list
                ;;
        esac
    fi
}

function add_installed() {
    local line="${APP} ${DEFVER} ${METHOD}"
    cat "${ETC_DIR}/installed" - <<< "${line}" | ${ELEVATE} sort -t " " -k 1 -u -o "${ETC_DIR}/installed"
}

function remove_installed() {
    ${ELEVATE} sed -i "/^${1} /d" "${ETC_DIR}/installed"
}

function list_apps() {
    list_local_apps; list_repo_apps
    declare -F | sed -n -e 's|declare -f deb_|00-builtin/|gp'
}

function populate_apps() {
    declare -ga APPS
    declare -gA APPNAME2FULL
    local APP

    mapfile -t APPS < <(list_apps | sort -t / -k 2 -u)
    # set, unless key already exists (only first match counts)
    for APP in "${APPS[@]}"; do : "${APPNAME2FULL["${APP##*/}"]:=${APP}}"; done
}

function deb_deb-get() {
    DEFVER=1
    ARCHS_SUPPORTED="amd64 arm64 armhf i386"
    get_github_releases "wimpysworld/deb-get"
    if [ "${ACTION}" != "prettylist" ]; then
        URL="$(grep "browser_download_url.*\.deb\"" "${CACHE_FILE}" | head -n1 | cut -d'"' -f4)"
        VERSION_PUBLISHED="$(echo "${URL}" | cut -d'_' -f2)"
    fi
    PRETTY_NAME="deb-get"
    WEBSITE="https://github.com/wimpysworld/deb-get"
    SUMMARY="'apt-get' functionality for .debs published in 3rd party repositories or via direct download package."
}

function parse_machine() {
export HOST_CPU="$(uname -m)"
case "${HOST_CPU}" in
  aarch64|armv7l|x86_64) export HOST_ARCH="$(dpkg --print-architecture)";;
  *) fancy_message fatal "${HOST_CPU} is not supported. Quitting.";;
esac

OS_ID=$(lsb_release --id --short)
case "${OS_ID}" in
  Debian) OS_ID_PRETTY="Debian";;
  Devuan) OS_ID_PRETTY="Devuan";;
  elementary|Elementary) OS_ID_PRETTY="elementary OS";;
  Linuxmint) OS_ID_PRETTY="Linux Mint";;
  Neon) OS_ID_PRETTY="KDE Neon";;
  Pop) OS_ID_PRETTY="Pop!_OS";;
  Ubuntu) OS_ID_PRETTY="Ubuntu";;
  Zorin) OS_ID_PRETTY="Zorin OS";;
  *)
    OS_ID_PRETTY="${OS_ID}"
    fancy_message warn "${OS_ID} is not supported."
  ;;
esac

OS_CODENAME=$(lsb_release --codename --short)

if [ -e /etc/os-release ]; then
    OS_RELEASE=/etc/os-release
elif [ -e /usr/lib/os-release ]; then
    OS_RELEASE=/usr/lib/os-release
else
    fancy_message fatal "os-release not found. Quitting"
fi

UPSTREAM_ID="$(sed -n -e 's/^ID=//p' "${OS_RELEASE}")"

# Fallback to ID_LIKE if ID was not 'ubuntu' or 'debian'
if ! [[ ' ubuntu debian ' =~ " ${UPSTREAM_ID} " ]]; then
    UPSTREAM_ID_LIKE="$(sed -n -e 's/^ID_LIKE=//p' "${OS_RELEASE}" | cut -d \" -f 2)"

    if [[ " ${UPSTREAM_ID_LIKE} " =~ " ubuntu " ]]; then
        UPSTREAM_ID=ubuntu
    elif [[ " ${UPSTREAM_ID_LIKE} " =~ " debian " ]]; then
        UPSTREAM_ID=debian
    else
        fancy_message fatal "${OS_ID_PRETTY} ${OS_CODENAME^} is not supported because it is not derived from a supported Debian or Ubuntu release."
    fi
fi

# Debian Sid & Testing don't always have their own unique os-release file. It is often the same as the previous Debian stable.
# So we first check /etc/debian_version to make sure we're not running Stable.
# If not, we will then use apt-cache policy to distinguish between Debian Sid and Debian Testing.
local ETC_DEB_VER="$(< /etc/debian_version)"
if [[ "${UPSTREAM_ID}" == "debian" ]] && grep -q "sid" <<< "${ETC_DEB_VER}"; then
    if apt-cache policy | grep -q -i -e "o=debian.*n=${ETC_DEB_VER%%/*}" -e "o=debian.*n=testing"; then
        UPSTREAM_CODENAME="${ETC_DEB_VER%%/*}"
        OS_CODENAME="${UPSTREAM_CODENAME}"
        DEBIAN_TESTING="${UPSTREAM_CODENAME}"
    else
        UPSTREAM_CODENAME="sid"
        OS_CODENAME="sid"
    fi
else
    local codename
    for codename in UBUNTU_CODENAME DEBIAN_CODENAME VERSION_CODENAME; do
        UPSTREAM_CODENAME=$(sed -n -e "/^$codename=/ {s/^$codename=//p;q}" "${OS_RELEASE}")
        [ -z "${UPSTREAM_CODENAME}" ] || break
    done
fi

case "${UPSTREAM_CODENAME}" in
    buster)   UPSTREAM_RELEASE="10";;
    bullseye) UPSTREAM_RELEASE="11";;
    bookworm) UPSTREAM_RELEASE="12";;
    trixie)   UPSTREAM_RELEASE="13";;
    forky)    UPSTREAM_RELEASE="14";;
    sid)      UPSTREAM_RELEASE="unstable";;
    focal)    UPSTREAM_RELEASE="20.04";;
    jammy)    UPSTREAM_RELEASE="22.04";;
    noble)    UPSTREAM_RELEASE="24.04";;
    oracular) UPSTREAM_RELEASE="24.10";;
    plucky)   UPSTREAM_RELEASE="25.04";;
    questing) UPSTREAM_RELEASE="25.10";;

    *)  #Devuan
        if [[ "${UPSTREAM_ID}" == "debian" ]]; then
            UPSTREAM_RELEASE="$(grep -E -o -m 1 "^[0-9]+" "/etc/debian_version")"
        fi
        case "${UPSTREAM_RELEASE}" in
            10) UPSTREAM_CODENAME="buster";;
            11) UPSTREAM_CODENAME="bullseye";;
            12) UPSTREAM_CODENAME="bookworm";;
            13) UPSTREAM_CODENAME="trixie";;
            14) UPSTREAM_CODENAME="forky";;
             *) fancy_message fatal "${OS_ID_PRETTY} ${OS_CODENAME^} is not supported because it is not derived from a supported Debian or Ubuntu release.";;
        esac
esac
}

function dg_action_cache() {
        ls -lh "${CACHE_DIR}/"
}
function dg_action_clean() {
        elevate_privs
        ${ELEVATE} rm -fv "${CACHE_DIR}"/*.deb
        ${ELEVATE} rm -fv "${CACHE_DIR}"/*.json*
        ${ELEVATE} rm -fv "${CACHE_DIR}"/*.html
        ${ELEVATE} rm -fv "${CACHE_DIR}"/*.txt

}
function dg_action_show() {
        for APP in "${@,,}"; do
            FULL_APP=${APPNAME2FULL[$APP]}
            if [ -z "${FULL_APP}" ]; then
                fancy_message error "${APP} is not a supported application."
                ACTION="list"
                list_debs "" --raw >&2
                exit 1
            fi
            if validate_deb "${FULL_APP}"; then
                info_deb
            fi
        done
}
function dg_action_reinstall() { dg_action_install "$@"; }
function dg_action_install() {
        elevate_privs
        create_cache_dir
        create_etc_dir
        for APP in "${@,,}"; do
            FULL_APP=${APPNAME2FULL[$APP]}
            if [ -z "${FULL_APP}" ]; then
                fancy_message error "${APP} is not a supported application."
                ACTION="list"
                list_debs "" --raw >&2
                exit 1
            fi
            if validate_deb "${FULL_APP}"; then
                if [[ " ${ARCHS_SUPPORTED} " != *" ${HOST_ARCH} "* ]]; then
                    fancy_message fatal "${APP} is not supported on ${HOST_ARCH}."
                fi

                if [ -n "${CODENAMES_SUPPORTED}" ] && ! [[ " ${CODENAMES_SUPPORTED[*]} " =~ " ${UPSTREAM_CODENAME} " ]]; then
                    fancy_message fatal "${APP} is not supported on ${OS_ID_PRETTY} ${UPSTREAM_CODENAME^}."
                fi

                if [ "${METHOD}" == "ppa" ] && [ "${UPSTREAM_ID}" != "ubuntu" ]; then
                    fancy_message fatal "${APP} cannot be installed as PPAs are not supported on distros that are not derived from Ubuntu."
                fi

                case "${METHOD}" in
                    direct|github|gitlab|website) install_deb "${URL}";;
                    apt) install_apt;;
                    ppa) install_ppa;;
                esac
            fi
        done
}
function dg_action_list() {
        list_opt_1=""
        list_opt_2=""
        while [ -n "${1}" ]; do
            if [ "${1}" == --include-unsupported ]; then
                list_opt_1=--include-unsupported
            elif [[ " --raw --installed --not-installed --only-unsupported " =~ " ${1} " ]]; then
                list_opt_2="${1}"
            else
                fancy_message fatal "Unknown option supplied: ${1}"
            fi
            shift
        done
        list_debs "${list_opt_1}" "${list_opt_2}"
}
function dg_action_prettylist() {
        ACTION="prettylist"
        prettylist_debs "${1}"
}
function dg_action_csvlist() {
        ACTION="prettylist"
        csvlist_debs "${1}"
}
function dg_action_purge() {
        elevate_privs
        opt_remove_repo=""
        if [ "${1}" == --remove-repo ]; then
            opt_remove_repo=--remove-repo
            shift
        fi
        for APP in "${@,,}"; do
            FULL_APP=${APPNAME2FULL[$APP]}
            if [ -z "${FULL_APP}" ]; then
                FULL_APP="$(printf '%s\n' "${DEPRECATED_APPS[@]}" | grep -m 1 "/${APP}$")"
            fi
            if [ -z "${FULL_APP}" ]; then
                fancy_message error "${APP} is not a supported application."
                ACTION="list"
                list_debs "" --raw >&2
                exit 1
            fi
            if validate_deb "${FULL_APP}"; then
                remove_deb "${APP}" purge "${opt_remove_repo}"
            fi
        done
}
function dg_action_remove() {
        elevate_privs
        opt_remove_repo=""
        if [ "${1}" == --remove-repo ]; then
            opt_remove_repo=--remove-repo
            shift
        fi
        for APP in "${@,,}"; do
            FULL_APP=${APPNAME2FULL[$APP]}
            if [ -z "${FULL_APP}" ]; then
                FULL_APP="$(printf '%s\n' "${DEPRECATED_APPS[@]}" | grep -m 1 "/${APP}$")"
            fi
            if [ -z "${FULL_APP}" ]; then
                fancy_message error "${APP} is not a supported application."
                ACTION="list"
                list_debs "" --raw >&2
                exit 1
            fi
            if validate_deb "${FULL_APP}"; then
                remove_deb "${APP}" "" "${opt_remove_repo}"
            fi
        done
}
function dg_action_search() {
        local list_opt_1=''
        if [ "${1}" == --include-unsupported ]; then
            list_opt_1=$1
            shift
        fi
        if [ -z "${1}" ]; then
            fancy_message error "You must specify a pattern."
            usage >&2
            exit 1
        fi
        list_debs "$list_opt_1" --raw | grep -e "${1}"
}
function dg_action_update() {
        if   [ -n "${1}" ] && ! [[ ' --repos-only --quiet ' =~ "${1}" ]]; then
            fancy_message fatal "Unknown option supplied: ${1}"
        elif [ -n "${2}" ] && ! [[ ' --repos-only --quiet ' =~ "${2}" ]]; then
                fancy_message fatal "Unknown option supplied: ${2}"
        fi
        if [ -n "${3}" ] ; then
                    fancy_message error "Ignoring extra options from : ${3}"
        fi
        elevate_privs
        create_cache_dir
        create_etc_dir
        init_repos
        update_repos "$@"
        if [[ " $* " != *' --repos-only '* ]] ; then
            populate_apps
            for APP in "${INSTALLED_APPS[@]}"; do
                FULL_APP=${APPNAME2FULL[$APP]}
                if [ -n "${FULL_APP}" ]; then
                    if validate_deb "${FULL_APP}"; then
                        fix_installed
                    fi
                else
                    remove_installed "${APP}"
                fi
            done
            mapfile -t INSTALLED_APPS <"${ETC_DIR}/installed"; INSTALLED_APPS=(${INSTALLED_APPS[@]%% *})
            update_debs
        fi
}
function dg_action_upgrade() {
        elevate_privs
        create_cache_dir
        upgrade_debs "$@"
}
function dg_action_fix-installed() {
        if [ -n "${1}" ] && [ "${1}" != --old-apps ]; then
            fancy_message fatal "Unknown option supplied: ${1}"
        fi
        elevate_privs
        if [ "${1}" = --old-apps ]; then
            for APP in $(dpkg-query 2>/dev/null -Wf '${db:Status-abbrev}${Package}\n' "${APPS[*]##*/}" | sed -n -e 's/^ii //p'); do
                if validate_deb "$(printf '%s\n' "${APPS[@]}" | grep -m 1 "/${APP}$")"; then
                    fix_old_apps
                fi
            done
        else
            for APP in "${INSTALLED_APPS[@]}"; do
                FULL_APP="$(printf '%s\n' "${APPS[@]}" | grep -m 1 "/${APP}$")"
                if [ -n "${FULL_APP}" ]; then
                    if validate_deb "${FULL_APP}"; then
                        fix_installed
                    fi
                else
                    remove_installed "${APP}"
                fi
            done
        fi
}
function dg_action_version() { echo "${VERSION}"; }
function dg_action_help() { usage; }


if ((BASH_VERSINFO[0] < 4)); then
    fancy_message fatal "Sorry, you need bash 4.0 or newer to run $(basename "${0}")."
fi

if ! command -v lsb_release 1>/dev/null; then
  fancy_message fatal "lsb_release not detected. Quitting."
fi

export CACHE_DIR="/var/cache/deb-get"
readonly ETC_DIR="/etc/deb-get"
readonly MAIN_REPO_URL="https://raw.githubusercontent.com/wimpysworld/deb-get/main/01-main"

readonly USER_AGENT="Mozilla/5.0 (X11; Linux ${HOST_CPU}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
readonly USER_HOME="${HOME}"

readonly DEBGET_BIN=$(basename $0)

parse_machine

if [ -n "${1}" ]; then
    ACTION="${1,,}"
    shift
else
    fancy_message error "You must specify an action."
    usage >&2
    exit 1
fi
case "${ACTION}" in
    (pretty_list|csv_list) ACTION=${ACTION/_/} ;;
    (csv) ACTION=csvlist ;;
esac

case "${ACTION}" in (update|upgrade|show|reinstall|install|remove|purge|search|list|prettylist|csvlist|fix-installed)
    populate_apps
    mapfile -t APP_CONFLICTS < <(printf '%s\n' "${APPS[@]##*/}" | uniq --repeated)
    mapfile -t LOCAL_APPS < <(printf '%s\n' "${APPS[@]}" | grep "^99-local/")
    if [ -e "${ETC_DIR}/installed" ]; then
        mapfile -t INSTALLED_APPS <"${ETC_DIR}/installed"; INSTALLED_APPS=("${INSTALLED_APPS[@]%% *}")
    else
        INSTALLED_APPS=()
    fi
    readonly APP_CONFLICTS LOCAL_APPS
    ;;
esac

case "${ACTION}" in (install|reinstall|remove|purge|show)
        if [ -z "${1}" ]; then
            fancy_message error "You must specify an app:\n"
            ACTION="list"
            list_debs "" --raw >&2
            exit 1
        fi
        print_etc_overrides
        populate_deprecated_apps
        print_deprecated;;
esac

export ELEVATE=""

if declare -F "dg_action_$ACTION" >/dev/null; then
    "dg_action_$ACTION" "$@"
else
    fancy_message fatal "Unknown action supplied: ${ACTION}"
fi

if [[ ${PACKAGE_INSTALLATION_COUNT} -lt ${PACKAGE_INSTALLATION_TRIES} ]]; then
    exit 1
fi
